
/*
    TreeViewer - Cross-platform software to draw phylogenetic trees
    Copyright (C) 2023  Giorgio Bianchini, University of Bristol
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, version 3.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using PhyloTree;
using TreeViewer;
using VectSharp;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Reflection;
using Avalonia.Styling;

namespace NodeStates
{
    /// <summary>
    /// This module can be used to draw shapes at each node highlighting the "state" of the node. This can consist in a pie chart,
    /// a rectangle (useful for trees where _Rectangular_ coordinates are being used) or a wedge (useful for trees where _Circular_
    /// coordinates are being used).
    /// </summary>
    /// 
    /// <description>
    /// <![CDATA[
    /// ## Further information
    /// 
    /// The format for the attribute that contains the node states should be as follows:
    /// 
    /// * If the attribute is a `Number`, the value of the attribute will be taken as the state of the node with weight 100%.
    /// 
    /// * If the attribute is a `String`, the value of the attribute can be:
    /// 
    ///     * a single alphanumerical character (e.g. `A`): in this case, the state of the node will correspond to this character
    ///       with weight 100% and to any other state with weight 0%.
    /// 
    ///     * a list of `State:Weight` pairs, separated by commas and enclosed within curly brackets (e.g. `{A:0.5,B:0.5}`): in
    ///       this case, the state of the node will be a mixture with the provided weights. Weights are normalised on a per-node
    ///       basis (e.g. `{A:0.66,B:0.33}` is equivalent to `{A:20,B:10}`).
    /// 
    /// The most appropriate settings to use with this module to achieve the best results depend on the Coordinates module that is
    /// being used. Pie charts are appropriate to show states at each node of a tree drawn using any Coordinates module.
    /// Rectangles are most appropriate to show states at the leaf nodes for trees drawn with _Rectangular_ coordinates. Wedges are
    /// most appropriate to show leaf states for trees drawn using _Circular_ coordinates.
    /// 
    /// Here is an example of a tree that uses pie charts:
    ///
    /// <p align="center">
    /// <img height="800" src="" />
    /// </p>
    ///
    /// To obtain the best results for trees with _Rectangular_ coordinates, you should:
    /// 
    /// * Set [Show on](#show-on) to `Tips`.
    /// * Set the [Anchor](#anchor) to `Origin`.
    /// * Set the [Orientation reference](#orientation-reference) to `Branch`.
    /// * Set the [Position](#position) to a sufficient displacement on the `X` axis (e.g. `(1000, 0)`).
    /// * Set the [Type](#type) to `Rectangle`.
    /// 
    /// Here is an example of a tree obtained with these settings:
    /// <p align="center">
    /// <img height="800" src="" />
    /// </p>
    ///
    /// To obtain the best results for trees with _Circular_ coordinates, you should:
    /// 
    /// * Set [Show on](#show-on) to `Tips`.
    /// * Set the [Anchor](#anchor) to `Origin`.
    /// * Set the [Orientation reference](#orientation-reference) to `Branch`.
    /// * Set the [Position](#position) to a displacement on the `X` axis that is slightly larger than the outer radius of the
    /// circular coordinates (e.g. `(1100, 0)` if the outer radius is `1000`).
    /// * Set the [Type](#type) to `Wedge`.
    /// * Set the [Width](#width) to the [Suggested width](#suggested-width).
    /// 
    /// Here is an example of a tree obtained with these settings:
    /// <p align="center">
    /// <img height="800" src="" />
    /// </p>
    ///
    /// ]]>
    /// </description>

    public static class MyModule
    {
        public const string Name = "Node states";
        public const string HelpText = "Draws node states based on attributes.";
        public const string Author = "Giorgio Bianchini";
        public static Version Version = new Version("1.3.0");
        public const string Id = "0512b822-044d-4c13-b3bb-bca494c51daa";
        public const ModuleTypes ModuleType = ModuleTypes.Plotting;

        private static string Icon16Base64 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABqSURBVDhPY2QgEhQVFf2HMskDFBuACxDlhc7IrXDby5d7o+hhgdJwgO7Uvr4+vJZgGAAChDThBTQLLFwGozjVq3YjXNG2Zn8UOZwGwCRA/sZnAC5AtAvoB0iNBSYoTTag2ACMgCLNCwwMALXiKhGFquq2AAAAAElFTkSuQmCC";
        private static string Icon24Base64 = "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACtSURBVEhLY2SgABQVFTUAqXoIDztggtLkAryGUwyAPvgPwlAuVkCpDwYekB3JnZFbUYKmfLk3VrNwWoAvhfT19TESawG+OKBKCsHnA7ALQa4FC6ABYn2AExCTBIkBFCVTYhxB83xAWrihAWKCEKsFXrUbUTRua/bHqg5oAcHCjhHdFaBUQ6wFxICBiQNq+mDgADFpnBhA8zgY+hbgTB3UCH8QwOeDRihNAWBgAAAiXEGZq2aFZgAAAABJRU5ErkJggg==";
        private static string Icon32Base64 = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAC+SURBVFhHY2SgMigqKmoAUvUQHmHABKWpCYi2nCYAGAL/QRjKJQhoEQIkgQF3wIADquWCzsitWOO9fLk3XjsGPAoIhgAx+bqvr4+RliFA03xNTAiAfQbyJVgABxiyaYAgILVkIxVQPQRIdfCAR8HgLwdIBaSmF5wO8KrdiNWgbc3+eB0NdABJDRKqO4BUwIgtyECFDr0cMHgT4YgJgcEPSC1aSQWjRfGoAwbcAQQLFVrmABAgJgQaoTQNAAMDAI1TRaieEa2dAAAAAElFTkSuQmCC";

        public static Page GetIcon(double scaling)
        {
            byte[] bytes;

            if (scaling <= 1)
            {

                bytes = Convert.FromBase64String(Icon16Base64);
            }
            else if (scaling <= 1.5)
            {
                bytes = Convert.FromBase64String(Icon24Base64);
            }
            else
            {
                bytes = Convert.FromBase64String(Icon32Base64);
            }

            IntPtr imagePtr = Marshal.AllocHGlobal(bytes.Length);
            Marshal.Copy(bytes, 0, imagePtr, bytes.Length);

            RasterImage icon;

            try
            {
                icon = new VectSharp.MuPDFUtils.RasterImageStream(imagePtr, bytes.Length, MuPDFCore.InputFileTypes.PNG);
            }
            catch (Exception ex)
            {
                throw ex.InnerException;
            }
            finally
            {
                Marshal.FreeHGlobal(imagePtr);
            }

            Page pag = new Page(16, 16);
            pag.Graphics.DrawRasterImage(0, 0, 16, 16, icon);

            return pag;
        }

        public static List<(string, string)> GetParameters(TreeNode tree)
        {
            return new List<(string, string)>()
            {
                ( "Window", "Window:" ),
                ( "StateData", "InstanceStateData:" ),
                
                /// <param name="Show on:">
                /// This parameter determines on which nodes the states are shown. If the value is `Leaves`, the
                /// states are only shown for terminal nodes (nodes with no child nodes). If the value is `Internal
                /// nodes` the states are shown only for internal nodes (nodes which have at least one child).
                /// If the value is `All nodes`, states are shown for both leaves and internal nodes.
                /// </param>
                ( "Show on:", "ComboBox:0[\"Tips\",\"Internal nodes\",\"All nodes\"]" ),
                
                /// <param name="Exclude cartoon nodes">
                /// This parameter determines whether states are shown for nodes which have been "cartooned" or collapsed.
                /// If the check box is checked, states are not shown for nodes that have been "cartooned".
                /// </param>
                ( "Exclude cartoon nodes", "CheckBox:true" ),

                ( "Position", "Group:3"),
                
                /// <param name="Anchor:">
                /// This parameter determines the anchor for the centre of the state shape. If the value is `Node`, the centre of the shape is anchored to the corresponding node.
                /// If the value is `Mid-branch`, the centre of the shape is aligned with the midpoint of the branch connecting the node to its parent.
                /// If the value is `Origin`, the alignment depends on the current Coordinates module:
                /// 
                /// +------------------------------------+------------------------------------------------------------------------+
                /// | Coordinates module                 | Origin                                                                 |
                /// +====================================+========================================================================+
                /// | _Rectangular_                      | A point corresponding to the projection of the node on a line          |
                /// |                                    | perpedicular to the direction in which the tree expands and passing    |
                /// |                                    | through the root node. Usually (i.e. if the tree is horizontal), this  |
                /// |                                    | means a point with the same horizontal coordinate as the root node and |
                /// |                                    | the same vertical coordinate as the current node.                      |
                /// +------------------------------------+------------------------------------------------------------------------+
                /// | _Radial_                           | The root node.                                                         |
                /// |                                    |                                                                        |
                /// |                                    |                                                                        |
                /// +------------------------------------+------------------------------------------------------------------------+
                /// | _Circular_                         | The root node.                                                         |
                /// |                                    |                                                                        |
                /// |                                    |                                                                        |
                /// |                                    |                                                                        |
                /// |                                    |                                                                        |
                /// |                                    |                                                                        |
                /// +------------------------------------+------------------------------------------------------------------------+
                /// </param>
                ( "Anchor:", "ComboBox:0[\"Node\",\"Mid-branch\",\"Origin\"]" ),
                
                /// <param name="Orientation reference:">
                /// This parameter determines the direction along which the offset of the centre of the shape from the anchor is computed. If the value is `Horizontal`,
                /// the offset `X` coordinate of the offset corresponds to an horizontal displacement and the `Y` coordinate to a vertical displacement; if the value is
                /// `Branch`, the `X` coordinate corresponds to a shift in the direction of the branch, while the `Y` coordinate corresponds to a shift in a direction
                /// perpendicular to the branch.
                /// </param>
                ( "Orientation reference:", "ComboBox:1[\"Horizontal\",\"Branch\"]" ),
                
                /// <param name="Position:">
                /// This parameter determines how shifted from the anchor point the shape is. The `X` coordinate corresponds to the line determined by the [Orientation reference](#orientation-reference);
                /// the `Y` coordinate corresponds to the line perpendicular to this.
                /// </param>
                ( "Position:", "Point:[0,0]" ),

                ( "Appearance", "Group:7"),
                
                /// <param name="Type:">
                /// This parameter determines the kind of plot that is used to display the states for the nodes. If the value is `Pie chart`, a small pie
                /// chart is drawn at each node, with each slice of the pie corresponding to a possible state. If the value is `Rectangle`, a rectangle is
                /// drawn for each node; the different states are represented by differently coloured areas in the triangle. If the value is `Wedge`, the
                /// result is similar to the `Rectangle` case, except that the rectangle is cut in the shape of circular ring sector.
                /// </param>
                ( "Type:", "ComboBox:0[\"Pie chart\",\"Rectangle\",\"Wedge\"]" ),
                
                /// <param name="Width:">
                /// This parameter determines the width of the shape. This can be determined on a node-by-node basis using an attribute.
                /// </param>
                ( "Width:", "NumericUpDownByNode:8[\"0\",\"Infinity\"," + System.Text.Json.JsonSerializer.Serialize(Modules.DefaultAttributeConvertersToDouble[1]) + ",\"StateWidth\",\"Number\",\"true\"]" ),
                
                /// <param name="Suggested width:" default="" value-header="">
                /// If the Coordinates module is set as `Circular`, the [Type](#type) is set as `Wedge`, and the [Anchor](#anchor) is set
                /// as `Origin`, this read-only control shows the suggested width for the wedge, based on the number of taxa in the tree and the position.
                /// </param>
                ( "Suggested width:", "NumericUpDown:8[\"0\",\"Infinity\",\"1\",\"0\"]" ),
                
                /// <param name="Height:">
                /// This parameter determines the height of the shape. This can be determined on a node-by-node basis using an attribute.
                /// </param>
                ( "Height:", "NumericUpDownByNode:8[\"0\",\"Infinity\"," + System.Text.Json.JsonSerializer.Serialize(Modules.DefaultAttributeConvertersToDouble[1]) + ",\"StateHeight\",\"Number\",\"true\"]" ),
                
                /// <param name="Angle:">
                /// This parameter determines the angle by which the coloured areas representing the states of the node are rotated within the shape. E.g.
                /// if the [Type](#type) is `Rectangle` and this angle is `0°`, the different states will be represented as vertical bands; if the angle is
                /// `90°`, the states will be represended as horizontal bands.
                /// </param>
                ( "Angle:", "Slider:0[\"0\",\"360\",\"0°\"]" ),
                
                /// <param name="Stroke thickness:">
                /// This parameter determines the thickness of the stroke surrounding the state shape.
                /// </param>
                ( "Stroke thickness:", "NumericUpDown:0[\"0\",\"Infinity\"]" ),
                
                /// <param name="Stroke colour:">
                /// This parameter determines the colour of the stroke surrounding the state shape.
                /// </param>
                ( "Stroke colour:", "Colour:[0,0,0,255]" ),

                ( "Characters and states", "Group:5"),
                
                /// <param name="Total characters:">
                /// This text box shows the total number of characters for which data is contained in the selected attribute.
                /// </param>
                ( "Total characters:", "TextBox:" ),

                /// <param name="Enabled characters:" default="">
                /// This parameter contains a script that is used to determine which characters are enabled in the plot. Each character is associated to a
                /// boolean value - `true` means that the character is enabled, while `false` means that the character is not enabled. This can be changed
                /// more easily using the [Wizard edit enabled characters](#wizard-edit-enabled-characters) button.
                /// </param>
                ( "Enabled characters:", "SourceCode:" + GetDefaultEnabledCharactersCode(new string[0][]) ),
                
                /// <param name="Wizard edit state colours">
                /// This button opens a window that can be used to specify which characters are enabled using a graphical interface.
                /// </param>
                ( "Wizard edit enabled characters", "Button:" ),
                
                /// <param name="State colours:">
                /// This parameter is used to determine the colour associated to each state. While this uses a "Colour by node" control, the colours are actually
                /// determined based on the names of the states, rather than on attributes of the tree. The colours associated with each state can be changed
                /// (or additional states can be added) by modifying the formatter code for this parameter.
                /// 
                /// The default formatter used if the states are specified as strings is the following:
                /// 
                /// ```CSharp
                /// public static Colour? Format(object attribute)
                /// {
                ///     if (attribute is string state)
                ///     {
                ///         switch (state)
                ///         {
                ///             case "A":
                ///                 return Colour.FromRgb(213, 94, 0);
                ///             case "B":
                ///                 return Colour.FromRgb(0, 114, 178);
                ///             case "C":
                ///                 return Colour.FromRgb(0, 158, 115);
                ///             case "0":
                ///                 return Colour.FromRgb(213, 94, 0);
                ///             case "1":
                ///                 return Colour.FromRgb(0, 114, 178);
                ///             case "2":
                ///                 return Colour.FromRgb(0, 158, 115);
                ///             case "Y":
                ///                 return Colour.FromRgb(0, 158, 115);
                ///             case "N":
                ///                 return Colour.FromRgb(220, 220, 220);
                ///             default:
                ///                 return null;
                ///         }
                ///     }
                ///     else
                ///     {
                ///         return null;
                ///     }
                /// }
                /// ```
                /// 
                /// The colour associated to each state can be changed by changing the RGB values in the `Colour.FromRgb` method calls. The possible states can
                /// be changed by modifying the `case` labels in the `switch` statement (e.g. replacing `"A"` with something else, or adding new `case` lines for
                /// additional states).
                /// 
                /// If the states are specified as numbers (e.g in the case of continuous characters), a standard number formatter can be used, by setting an
                /// appropriate maximum and minimum value, and using a gradient to associate the colour to each state.
                /// </param>
                ( "State colours:", "ColourByNode:[" + System.Text.Json.JsonSerializer.Serialize(DefaultStateColourConverter) + ",\"(N/A)\",\"String\",\"220\",\"220\",\"220\",\"255\",\"true\"]" ),

                /// <param name="Wizard edit state colours">
                /// This button opens a window that can be used to specify the state colours using a graphical interface.
                /// </param>
                ( "Wizard edit state colours", "Button:" ),

                ( "Attribute", "Group:3" ),
                
                /// <param name="Attribute:">
                /// This parameter determines the attribute that contains the state associated to each node.
                /// </param>
                ( "Attribute:", "AttributeSelector:Name" ),
                
                /// <param name="Attribute type:">
                /// This parameter specifies the type of the attribute that contains the state associated to each node. You should use an attribute of type
                /// `String` for discrete character states, and an attribute of type `Number` for continuous character states.
                /// </param>
                ( "Attribute type:", "AttributeType:String"),
                
                /// <param name="Missing data:">
                /// This parameter determines how nodes for which the specified attribute is not present are treated. If the value is `Ambiguous state`, in these
                /// nodes it is assumed that all the states are possible with equal probability. If the value is `Unknown state`, the state for the node is assumed
                /// to be unknown, and it is drawn using the default [State colour](#state-colours) (e.g. grey in the default settings).
                /// </param>
                ( "Missing data:", "ComboBox:0[\"Ambiguous state\",\"Unknown state\"]" ),
            };
        }

        private static string DefaultStateColourConverter = @"public static Colour? Format(object attribute)
{
    if (attribute is string state)
    {
        switch (state)
        {
            case ""A"":
                return Colour.FromRgb(213, 94, 0);
            case ""B"":
                return Colour.FromRgb(0, 114, 178);
            case ""C"":
                return Colour.FromRgb(0, 158, 115);
            case ""0"":
                return Colour.FromRgb(213, 94, 0);
            case ""1"":
                return Colour.FromRgb(0, 114, 178);
            case ""2"":
                return Colour.FromRgb(0, 158, 115);
            case ""Y"":
                return Colour.FromRgb(0, 158, 115);
            case ""N"":
                return Colour.FromRgb(220, 220, 220);
            default:
                return null;
        }
    }
    else
    {
        return null;
    }
}";

        public static bool OnParameterChange(object tree, Dictionary<string, object> previousParameterValues, Dictionary<string, object> currentParameterValues, out Dictionary<string, ControlStatus> controlStatus, out Dictionary<string, object> parametersToChange)
        {
            parametersToChange = new Dictionary<string, object>() { { "Wizard edit state colours", false }, { "Wizard edit enabled characters", false } };

            if ((int)currentParameterValues["Type:"] == 2 && (int)currentParameterValues["Anchor:"] == 2 && ((InstanceStateData)currentParameterValues["StateData"]).CoordinateModule().Id == "92aac276-3af7-4506-a263-7220e0df5797")
            {
                controlStatus = new Dictionary<string, ControlStatus>()
                {
                    { "Suggested width:", ControlStatus.Disabled }
                };

                double suggestedWidth = 2 * Math.PI / ((TreeNode)tree).GetLeaves().Count * ((Point)currentParameterValues["Position:"]).Modulus() * 1.1;

                if ((double)currentParameterValues["Suggested width:"] != suggestedWidth)
                {
                    parametersToChange.Add("Suggested width:", suggestedWidth);
                }
            }
            else
            {
                controlStatus = new Dictionary<string, ControlStatus>()
                {
                    { "Suggested width:", ControlStatus.Hidden }
                };
            }

            controlStatus["Total characters:"] = ControlStatus.Disabled;

            void checkTotalAndEnabledCharacters(Dictionary<string, object> parametersToChange, Dictionary<string, ControlStatus> controlStatus, bool changedAttribute)
            {
                try
                {
                    InstanceStateData stateData = (InstanceStateData)currentParameterValues["StateData"];

                    string attributeName = (string)currentParameterValues["Attribute:"];

                    HashSet<string> allStatesString = new HashSet<string>();
                    List<TreeNode> nodes = stateData.TransformedTree.GetChildrenRecursive();

                    foreach (TreeNode node in nodes)
                    {
                        if (node.Attributes.TryGetValue(attributeName, out object attributeObject) && attributeObject is string attributeValue && !string.IsNullOrEmpty(attributeValue))
                        {
                            GetStatesString(attributeValue, allStatesString);
                        }
                    }

                    int characterCount = int.MaxValue;
                    bool foundAny = false;

                    foreach (string sr in allStatesString)
                    {
                        if (!string.IsNullOrEmpty(sr))
                        {
                            characterCount = Math.Min(characterCount, sr.Split('|').Length);
                            foundAny = true;
                        }
                    }

                    if (!foundAny)
                    {
                        characterCount = 0;
                    }

                    HashSet<string>[] multiStates = new HashSet<string>[characterCount];

                    for (int i = 0; i < characterCount; i++)
                    {
                        multiStates[i] = new HashSet<string>();
                    }

                    foreach (string sr in allStatesString)
                    {
                        if (!string.IsNullOrEmpty(sr))
                        {
                            string[] splitState = sr.Split('|');

                            for (int i = 0; i < splitState.Length; i++)
                            {
                                multiStates[i].Add(splitState[i]);
                            }
                        }
                    }

                    string[][] splitStates = (from el in multiStates select el.ToArray()).ToArray();

                    string statesString = characterCount.ToString();

                    if (characterCount <= 1)
                    {
                        controlStatus["Enabled characters:"] = ControlStatus.Hidden;
                        controlStatus["Wizard edit enabled characters"] = ControlStatus.Hidden;
                    }
                    else
                    {
                        controlStatus["Enabled characters:"] = ControlStatus.Enabled;
                        controlStatus["Wizard edit enabled characters"] = ControlStatus.Enabled;
                    }

                    if (statesString != (string)currentParameterValues["Total characters:"] || changedAttribute)
                    {
                        parametersToChange["Total characters:"] = statesString;

                        string code = GetDefaultStateColours(splitStates);

                        object[] formatterParams = new object[2] { code, false };

                        ColourFormatterOptions cfo = new ColourFormatterOptions(code, formatterParams) { AttributeName = "(N/A)", AttributeType = "String", DefaultColour = Colour.FromRgb(220, 220, 220) };

                        if (currentParameterValues["State colours:"] == previousParameterValues["State colours:"])
                        {
                            parametersToChange["State colours:"] = cfo;
                        }

                        if (currentParameterValues["Enabled characters:"] == previousParameterValues["Enabled characters:"])
                        {
                            parametersToChange["Enabled characters:"] = new CompiledCode(GetDefaultEnabledCharactersCode(splitStates));
                        }
                    }
                    else if (currentParameterValues["Enabled characters:"] != previousParameterValues["Enabled characters:"])
                    {
                        Assembly assembly = ((CompiledCode)currentParameterValues["Enabled characters:"]).CompiledAssembly;

                        object[] args = new object[] { splitStates };

                        bool[] enabledCharacters = (bool[])ModuleMetadata.GetTypeFromAssembly(assembly, "CustomCode").InvokeMember("GetEnabledCharacters", BindingFlags.Default | BindingFlags.InvokeMethod, null, null, args);

                        string[][] enabledCharacterStates = GetEnabledStates(splitStates, enabledCharacters).ToArray();

                        string code = GetDefaultStateColours(enabledCharacterStates);

                        object[] formatterParams = new object[2] { code, false };

                        ColourFormatterOptions cfo = new ColourFormatterOptions(code, formatterParams) { AttributeName = "(N/A)", AttributeType = "String", DefaultColour = Colour.FromRgb(220, 220, 220) };

                        if (currentParameterValues["State colours:"] == previousParameterValues["State colours:"])
                        {
                            parametersToChange["State colours:"] = cfo;
                        }
                    }
                }
                catch { }
            }





            if ((string)currentParameterValues["Attribute type:"] == "String")
            {
                controlStatus["Wizard edit state colours"] = ControlStatus.Enabled;
                controlStatus["Total characters:"] = ControlStatus.Disabled;
                controlStatus["Enabled characters:"] = ControlStatus.Enabled;
                controlStatus["Wizard edit enabled characters"] = ControlStatus.Enabled;

                checkTotalAndEnabledCharacters(parametersToChange, controlStatus, false);
            }
            else
            {
                controlStatus["Wizard edit state colours"] = ControlStatus.Hidden;
                controlStatus["Total characters:"] = ControlStatus.Hidden;
                controlStatus["Enabled characters:"] = ControlStatus.Hidden;
                controlStatus["Wizard edit enabled characters"] = ControlStatus.Hidden;
            }

            if ((string)previousParameterValues["Attribute:"] != (string)currentParameterValues["Attribute:"])
            {
                string attributeName = (string)currentParameterValues["Attribute:"];

                string attrType = ((TreeNode)tree).GetAttributeType(attributeName);

                if (!string.IsNullOrEmpty(attrType) && (string)previousParameterValues["Attribute type:"] == (string)currentParameterValues["Attribute type:"])
                {
                    parametersToChange.Add("Attribute type:", attrType);

                    if (attrType == "String")
                    {
                        controlStatus["Wizard edit state colours"] = ControlStatus.Enabled;
                        controlStatus["Total characters:"] = ControlStatus.Disabled;
                        controlStatus["Enabled characters:"] = ControlStatus.Enabled;
                        controlStatus["Wizard edit enabled characters"] = ControlStatus.Enabled;

                        checkTotalAndEnabledCharacters(parametersToChange, controlStatus, true);
                    }
                    else
                    {
                        controlStatus["Wizard edit state colours"] = ControlStatus.Hidden;
                        controlStatus["Total characters:"] = ControlStatus.Hidden;
                        controlStatus["Enabled characters:"] = ControlStatus.Hidden;
                        controlStatus["Wizard edit enabled characters"] = ControlStatus.Hidden;
                    }
                }
            }



            if ((bool)currentParameterValues["Wizard edit state colours"])
            {
                //ShowWizardEditWindow((Avalonia.Controls.Window)currentParameterValues["Window"])

                InstanceStateData stateData = (InstanceStateData)currentParameterValues["StateData"];

                string attributeName = (string)currentParameterValues["Attribute:"];

                string attrType = ((TreeNode)tree).GetAttributeType(attributeName);

                int moduleIndex = -1;

                List<PlottingModule> plottingModules = stateData.PlottingModules();
                for (int i = 0; i < plottingModules.Count; i++)
                {
                    if (plottingModules[i].Id == Id)
                    {
                        if ((string)stateData.GetPlottingModulesParameters(i)[Modules.ModuleIDKey] == (string)currentParameterValues[Modules.ModuleIDKey])
                        {
                            moduleIndex = i;
                            break;
                        }
                    }
                }

                ColourFormatterOptions StateColours = (ColourFormatterOptions)currentParameterValues["State colours:"];
                Colour defaultStateColour = StateColours.DefaultColour;
                Func<object, Colour?> stateColourFormatter = StateColours.Formatter;

                string[][] allStates;

                try
                {
                    HashSet<string> allStatesString = new HashSet<string>();
                    List<TreeNode> nodes = stateData.TransformedTree.GetChildrenRecursive();

                    foreach (TreeNode node in nodes)
                    {
                        if (node.Attributes.TryGetValue(attributeName, out object attributeObject) && attributeObject is string attributeValue && !string.IsNullOrEmpty(attributeValue))
                        {
                            GetStatesString(attributeValue, allStatesString);
                        }
                    }

                    int characterCount = int.MaxValue;
                    bool foundAny = false;

                    foreach (string sr in allStatesString)
                    {
                        if (!string.IsNullOrEmpty(sr))
                        {
                            characterCount = Math.Min(characterCount, sr.Split('|').Length);
                            foundAny = true;
                        }
                    }

                    if (!foundAny)
                    {
                        characterCount = 0;
                    }

                    if (characterCount > 1)
                    {
                        HashSet<string>[] multiStates = new HashSet<string>[characterCount];

                        for (int i = 0; i < characterCount; i++)
                        {
                            multiStates[i] = new HashSet<string>();
                        }

                        foreach (string sr in allStatesString)
                        {
                            if (!string.IsNullOrEmpty(sr))
                            {
                                string[] splitState = sr.Split('|');

                                for (int i = 0; i < splitState.Length; i++)
                                {
                                    multiStates[i].Add(splitState[i]);
                                }
                            }
                        }

                        string[][] splitStates = (from el in multiStates select el.ToArray()).ToArray();

                        object[] args = new object[] { splitStates };

                        Assembly assembly = ((CompiledCode)currentParameterValues["Enabled characters:"]).CompiledAssembly;
                        bool[] enabledCharacters = (bool[])ModuleMetadata.GetTypeFromAssembly(assembly, "CustomCode").InvokeMember("GetEnabledCharacters", BindingFlags.Default | BindingFlags.InvokeMethod, null, null, args);

                        List<List<string>> allPossibleStates = GetAllPossibleStates(splitStates, enabledCharacters);

                        allStatesString = new HashSet<string>(from el in allPossibleStates select string.Join("|", el));

                        allStates = GetEnabledStates(splitStates, enabledCharacters).ToArray();
                    }
                    else
                    {
                        allStates = new string[][] { allStatesString.ToArray() };
                    }

                    _ = ShowWizardEditWindow((Avalonia.Controls.Window)currentParameterValues["Window"], stateData, allStates, moduleIndex, defaultStateColour, stateColourFormatter);

                }
                catch { }

                //stateData.PlottingModulesParameterUpdater(0)
            }

            if ((bool)currentParameterValues["Wizard edit enabled characters"])
            {
                InstanceStateData stateData = (InstanceStateData)currentParameterValues["StateData"];

                string attributeName = (string)currentParameterValues["Attribute:"];

                string attrType = ((TreeNode)tree).GetAttributeType(attributeName);

                int moduleIndex = -1;

                List<PlottingModule> plottingModules = stateData.PlottingModules();
                for (int i = 0; i < plottingModules.Count; i++)
                {
                    if (plottingModules[i].Id == Id)
                    {
                        if ((string)stateData.GetPlottingModulesParameters(i)[Modules.ModuleIDKey] == (string)currentParameterValues[Modules.ModuleIDKey])
                        {
                            moduleIndex = i;
                            break;
                        }
                    }
                }

                ColourFormatterOptions StateColours = (ColourFormatterOptions)currentParameterValues["State colours:"];
                Colour defaultStateColour = StateColours.DefaultColour;
                Func<object, Colour?> stateColourFormatter = StateColours.Formatter;

                HashSet<string> allStatesString = new HashSet<string>();

                try
                {
                    List<TreeNode> nodes = stateData.TransformedTree.GetChildrenRecursive();

                    foreach (TreeNode node in nodes)
                    {
                        if (node.Attributes.TryGetValue(attributeName, out object attributeObject) && attributeObject is string attributeValue && !string.IsNullOrEmpty(attributeValue))
                        {
                            GetStatesString(attributeValue, allStatesString);
                        }
                    }

                    int characterCount = int.MaxValue;
                    bool foundAny = false;

                    foreach (string sr in allStatesString)
                    {
                        if (!string.IsNullOrEmpty(sr))
                        {
                            characterCount = Math.Min(characterCount, sr.Split('|').Length);
                            foundAny = true;
                        }
                    }

                    if (!foundAny)
                    {
                        characterCount = 0;
                    }

                    if (characterCount > 1)
                    {
                        HashSet<string>[] multiStates = new HashSet<string>[characterCount];

                        for (int i = 0; i < characterCount; i++)
                        {
                            multiStates[i] = new HashSet<string>();
                        }

                        foreach (string sr in allStatesString)
                        {
                            if (!string.IsNullOrEmpty(sr))
                            {
                                string[] splitState = sr.Split('|');

                                for (int i = 0; i < splitState.Length; i++)
                                {
                                    multiStates[i].Add(splitState[i]);
                                }
                            }
                        }

                        string[][] splitStates = (from el in multiStates select el.ToArray()).ToArray();

                        Assembly assembly = ((CompiledCode)currentParameterValues["Enabled characters:"]).CompiledAssembly;

                        object[] args = new object[] { splitStates };

                        bool[] enabledCharacters = (bool[])ModuleMetadata.GetTypeFromAssembly(assembly, "CustomCode").InvokeMember("GetEnabledCharacters", BindingFlags.Default | BindingFlags.InvokeMethod, null, null, args);

                        string[][] enabledCharacterStates = GetEnabledStates(splitStates, enabledCharacters).ToArray();

                        _ = ShowWizardEditEnabledCharactersWindow((Avalonia.Controls.Window)currentParameterValues["Window"], stateData, splitStates, enabledCharacters, moduleIndex);
                    }
                }
                catch { }
            }

            return true;
        }

        public static Point[] PlotAction(TreeNode tree, Dictionary<string, object> parameterValues, Dictionary<string, Point> coordinates, Graphics graphics)
        {
            double minX = double.MaxValue;
            double maxX = double.MinValue;
            double minY = double.MaxValue;
            double maxY = double.MinValue;

            bool everUpdated = false;

            void updateMaxMin(Point pt)
            {
                everUpdated = true;
                minX = Math.Min(minX, pt.X);
                maxX = Math.Max(maxX, pt.X);
                minY = Math.Min(minY, pt.Y);
                maxY = Math.Max(maxY, pt.Y);
            }

            static double distance(Point p1, Point p2)
            {
                return Math.Sqrt((p1.X - p2.X) * (p1.X - p2.X) + (p1.Y - p2.Y) * (p1.Y - p2.Y));
            };

            static Point sumPoint(Point p1, Point p2)
            {
                return new Point(p1.X + p2.X, p1.Y + p2.Y);
            }

            static Point subtractPoint(Point p1, Point p2)
            {
                return new Point(p1.X - p2.X, p1.Y - p2.Y);
            }

            static Point multiplyPoint(Point p1, double scale)
            {
                return new Point(p1.X * scale, p1.Y * scale);
            }

            int showOn = (int)parameterValues["Show on:"];
            bool excludeCartoonNodes = (bool)parameterValues["Exclude cartoon nodes"];
            int anchor = (int)parameterValues["Anchor:"];
            int reference = (int)parameterValues["Orientation reference:"];

            int branchReference;

            if (coordinates.TryGetValue("68e25ec6-5911-4741-8547-317597e1b792", out _))
            {
                // Rectangular coordinates
                branchReference = 0;
            }
            else if (coordinates.TryGetValue("d0ab64ba-3bcd-443f-9150-48f6e85e97f3", out _))
            {
                // Circular coordinates
                branchReference = 2;
            }
            else
            {
                // Radial coordinates
                branchReference = 1;
            }

            Point delta = (Point)parameterValues["Position:"];


            int type = (int)parameterValues["Type:"];

            if (type == 2 && (branchReference != 2 || anchor != 2))
            {
                if (parameterValues.TryGetValue(Modules.WarningMessageControlID, out object action) && action is Action<string, string> setWarning)
                {
                    setWarning("Setting the `Type` to `Wedge` is only recommended when using _Circular_ coordinates and when the `Anchor` is set to `Origin`!", "");
                }
            }
            else
            {
                if (parameterValues.TryGetValue(Modules.WarningMessageControlID, out object action) && action is Action<string, string> setWarning)
                {
                    setWarning(null, null);
                }
            }


            NumberFormatterOptions Width = (NumberFormatterOptions)parameterValues["Width:"];
            double defaultWidth = Width.DefaultValue;
            Func<object, double?> widthFormatter = Width.Formatter;

            NumberFormatterOptions Height = (NumberFormatterOptions)parameterValues["Height:"];
            double defaultHeight = Height.DefaultValue;
            Func<object, double?> heightFormatter = Height.Formatter;

            double splitAngle = (double)parameterValues["Angle:"] / 180.0 * Math.PI;

            ColourFormatterOptions StateColours = (ColourFormatterOptions)parameterValues["State colours:"];
            Colour defaultStateColour = StateColours.DefaultColour;
            Func<object, Colour?> stateColourFormatter = StateColours.Formatter;

            Colour strokeColour = (Colour)parameterValues["Stroke colour:"];
            double strokeThickness = (double)parameterValues["Stroke thickness:"];


            string attributeName = (string)parameterValues["Attribute:"];
            string attributeType = (string)parameterValues["Attribute type:"];
            int missingDataAction = (int)parameterValues["Missing data:"];


            List<TreeNode> nodes = tree.GetChildrenRecursive();

            Dictionary<object, Colour> allStateColours = new Dictionary<object, Colour>();
            Dictionary<string, List<(object, double)>> states = new Dictionary<string, List<(object, double)>>();

            if (attributeType == "String")
            {
                HashSet<string> allStatesString = new HashSet<string>();

                List<(object, double)> ambiguousState = new List<(object, double)>();
                List<(object, double)> missingState = new List<(object, double)>();

                foreach (TreeNode node in nodes)
                {
                    if (node.Attributes.TryGetValue(attributeName, out object attributeObject) && attributeObject is string attributeValue && !string.IsNullOrEmpty(attributeValue))
                    {
                        if (attributeValue != "?")
                        {
                            states[node.Id] = (from el in GetStatesString(attributeValue, allStatesString) select ((object)el.Item1, el.Item2)).ToList();
                        }
                        else
                        {
                            states[node.Id] = ambiguousState;
                        }
                    }
                    else
                    {
                        if (missingDataAction == 0)
                        {
                            states[node.Id] = ambiguousState;
                        }
                        else if (missingDataAction == 1)
                        {
                            states[node.Id] = missingState;
                        }
                    }
                }

                int characterCount = int.MaxValue;
                bool foundAny = false;

                foreach (string sr in allStatesString)
                {
                    if (!string.IsNullOrEmpty(sr))
                    {
                        characterCount = Math.Min(characterCount, sr.Split('|').Length);
                        foundAny = true;
                    }
                }

                if (!foundAny)
                {
                    characterCount = 0;
                }

                if (characterCount > 1)
                {
                    Assembly assembly = ((CompiledCode)parameterValues["Enabled characters:"]).CompiledAssembly;

                    HashSet<string>[] multiStates = new HashSet<string>[characterCount];

                    for (int i = 0; i < characterCount; i++)
                    {
                        multiStates[i] = new HashSet<string>();
                    }

                    foreach (string sr in allStatesString)
                    {
                        if (!string.IsNullOrEmpty(sr))
                        {
                            string[] splitState = sr.Split('|');

                            for (int i = 0; i < splitState.Length; i++)
                            {
                                multiStates[i].Add(splitState[i]);
                            }
                        }
                    }

                    string[][] splitStates = (from el in multiStates select el.ToArray()).ToArray();

                    object[] args = new object[] { splitStates };

                    bool[] enabledCharacters = (bool[])ModuleMetadata.GetTypeFromAssembly(assembly, "CustomCode").InvokeMember("GetEnabledCharacters", BindingFlags.Default | BindingFlags.InvokeMethod, null, null, args);

                    List<List<string>> allPossibleStates = GetAllPossibleStates(splitStates, enabledCharacters);

                    foreach (KeyValuePair<string, List<(object, double)>> kvp in states)
                    {
                        List<(object, double)> marginalStates = GetMarginalStates(allPossibleStates, enabledCharacters, kvp.Value);
                        kvp.Value.Clear();
                        kvp.Value.AddRange(marginalStates);
                    }

                    allStatesString = new HashSet<string>(from el in allPossibleStates select string.Join("|", el));
                }

                ambiguousState.AddRange(from el in allStatesString select ((object)el, 1.0));
                foreach (string sr in allStatesString)
                {
                    allStateColours[sr] = stateColourFormatter(sr) ?? defaultStateColour;
                }




            }
            else if (attributeType == "Number")
            {
                List<(object, double)> ambiguousState = new List<(object, double)>();
                HashSet<double> allStatesDouble = new HashSet<double>();

                foreach (TreeNode node in nodes)
                {
                    if (node.Attributes.TryGetValue(attributeName, out object attributeObject) && attributeObject is double attributeValue)
                    {
                        if (!double.IsNaN(attributeValue))
                        {
                            states[node.Id] = new List<(object, double)>() { (attributeValue, 1.0) };
                            allStatesDouble.Add(attributeValue);
                        }
                        else
                        {
                            states[node.Id] = ambiguousState;
                        }
                    }
                    else
                    {
                        states[node.Id] = ambiguousState;
                    }
                }

                foreach (double d in allStatesDouble)
                {
                    allStateColours[d] = stateColourFormatter(d) ?? defaultStateColour;
                }
            }

            Point rootPoint = coordinates[Modules.RootNodeId];
            coordinates.TryGetValue("92aac276-3af7-4506-a263-7220e0df5797", out Point circularCenter);

            foreach (TreeNode node in nodes)
            {
                if ((showOn == 2 || (showOn == 0 && node.Children.Count == 0) || (showOn == 1 && node.Children.Count > 0)) && (!excludeCartoonNodes || !node.Attributes.ContainsKey("0c3400fd-8872-4395-83bc-a5dc5f4967fe")))
                {
                    double width = defaultWidth;

                    if (node.Attributes.TryGetValue(Width.AttributeName, out object widthAttributeObject) && widthAttributeObject != null)
                    {
                        width = widthFormatter(widthAttributeObject) ?? defaultWidth;
                    }

                    double height = defaultHeight;

                    if (node.Attributes.TryGetValue(Height.AttributeName, out object heightAttributeObject) && heightAttributeObject != null)
                    {
                        height = heightFormatter(heightAttributeObject) ?? defaultHeight;
                    }

                    Point point = coordinates[node.Id];
                    Point anglePoint = point;
                    double referenceAngle = 0;

                    if (reference == 0 && anchor == 0)
                    {

                    }
                    else
                    {
                        //Rectangular
                        if (branchReference == 0)
                        {
                            if (node.Parent != null)
                            {
                                Point parentPoint = coordinates[node.Parent.Id];

                                Point pA = coordinates[node.Parent.Children[0].Id];
                                Point pB = coordinates[node.Parent.Children[^1].Id];

                                double numerator = pA.Y + pB.Y - 2 * parentPoint.Y;
                                double denominator = pA.X + pB.X - 2 * parentPoint.X;

                                Point rectAnglePoint;

                                if (Math.Abs(numerator) > 1e-5 && Math.Abs(denominator) > 1e-5)
                                {
                                    double m = numerator / denominator;

                                    double x = (m * (parentPoint.Y - point.Y + m * point.X) + parentPoint.X) / (m * m + 1);
                                    double y = parentPoint.Y - (x - parentPoint.X) / m;

                                    rectAnglePoint = new Point(x, y);
                                }
                                else if (Math.Abs(numerator) > 1e-5)
                                {
                                    rectAnglePoint = new Point(point.X, parentPoint.Y);
                                }
                                else if (Math.Abs(denominator) > 1e-5)
                                {
                                    rectAnglePoint = new Point(parentPoint.X, point.Y);
                                }
                                else
                                {
                                    rectAnglePoint = point;
                                }

                                if (reference == 1)
                                {
                                    referenceAngle = Math.Atan2(point.Y - rectAnglePoint.Y, point.X - rectAnglePoint.X);
                                }

                                if (anchor == 1)
                                {
                                    anglePoint = rectAnglePoint;
                                }
                                else if (anchor == 2)
                                {
                                    Point branchVector = new Point(Math.Cos(referenceAngle), Math.Sin(referenceAngle));

                                    double d = (rootPoint.X - point.X) * branchVector.X + (rootPoint.Y - point.Y) * branchVector.Y;

                                    Point proj = new Point(point.X + d * branchVector.X, point.Y + d * branchVector.Y);

                                    anglePoint = new Point(-point.X + proj.X * 2, -point.Y + proj.Y * 2);
                                }

                            }
                            else
                            {
                                Point parentPoint = coordinates[Modules.RootNodeId];

                                if (anchor == 1)
                                {
                                    anglePoint = parentPoint;
                                }
                                else if (anchor == 2)
                                {
                                    anglePoint = new Point(-point.X + parentPoint.X * 2, -point.Y + parentPoint.Y * 2);
                                }

                                if (reference == 1)
                                {
                                    referenceAngle = Math.Atan2(point.Y - parentPoint.Y, point.X - parentPoint.X);
                                }
                            }
                        }
                        //Radial
                        else if (branchReference == 1)
                        {
                            Point parentPoint;

                            if (node.Parent != null)
                            {
                                parentPoint = coordinates[node.Parent.Id];
                            }
                            else
                            {
                                parentPoint = coordinates[Modules.RootNodeId];
                            }

                            if (anchor == 1)
                            {
                                anglePoint = parentPoint;
                            }

                            if (reference == 1)
                            {
                                referenceAngle = Math.Atan2(point.Y - parentPoint.Y, point.X - parentPoint.X);
                            }
                        }
                        //Circular
                        else if (branchReference == 2)
                        {
                            Point parentPoint;

                            if (node.Parent != null)
                            {
                                parentPoint = coordinates[node.Parent.Id];
                            }
                            else
                            {
                                parentPoint = coordinates[Modules.RootNodeId];
                            }

                            double myRadius = distance(point, circularCenter);
                            double parentRadius = distance(parentPoint, circularCenter);

                            Point realElbowPoint = sumPoint(point, multiplyPoint(subtractPoint(circularCenter, point), (myRadius - parentRadius) / myRadius));

                            if (anchor == 1)
                            {
                                anglePoint = realElbowPoint;
                            }
                            else if (anchor == 2)
                            {
                                anglePoint = new Point(-point.X + circularCenter.X * 2, -point.Y + circularCenter.Y * 2);
                            }

                            if (reference == 1)
                            {
                                referenceAngle = Math.Atan2(point.Y - realElbowPoint.Y, point.X - realElbowPoint.X);
                            }
                        }
                    }

                    if (double.IsNaN(referenceAngle))
                    {
                        referenceAngle = 0;
                    }

                    point = new Point((point.X + anglePoint.X) * 0.5, (point.Y + anglePoint.Y) * 0.5);

                    if (width > 0 && height > 0)
                    {
                        Point originalPoint = new Point(point.X + delta.X * Math.Cos(referenceAngle) - delta.Y * Math.Sin(referenceAngle), point.Y + delta.X * Math.Sin(referenceAngle) + delta.Y * Math.Cos(referenceAngle));

                        graphics.Save();
                        graphics.Translate(point);
                        graphics.Rotate(referenceAngle);

                        point = delta;

                        //Pie charts
                        if (type == 0)
                        {
                            double size = Math.Sqrt(width * height);

                            double scaleX = width / size;
                            double scaleY = height / size;

                            graphics.Scale(scaleX, scaleY);

                            point = new Point(point.X / scaleX, point.Y / scaleY);

                            List<(object, double)> nodeStates = states[node.Id];

                            if (nodeStates.Count > 0)
                            {
                                double totalProb = 0;

                                for (int i = 0; i < nodeStates.Count; i++)
                                {
                                    totalProb += nodeStates[i].Item2;
                                }

                                if (totalProb != 0)
                                {
                                    double currProb = 0;

                                    for (int i = 0; i < nodeStates.Count; i++)
                                    {
                                        double endProb = currProb + nodeStates[i].Item2;

                                        double startAngle = currProb / totalProb * 2 * Math.PI;
                                        double endAngle = endProb / totalProb * 2 * Math.PI;

                                        GraphicsPath fillPath = new GraphicsPath();
                                        fillPath.MoveTo(point);
                                        if (i < nodeStates.Count - 1 && allStateColours[nodeStates[i + 1].Item1].A == 1)
                                        {
                                            fillPath.Arc(point, size * 0.5, startAngle + splitAngle, (endProb + nodeStates[i + 1].Item2) / totalProb * 2 * Math.PI + splitAngle);
                                        }
                                        else
                                        {
                                            fillPath.Arc(point, size * 0.5, startAngle + splitAngle, endAngle + splitAngle);
                                        }

                                        fillPath.Close();

                                        Colour stateColour = allStateColours[nodeStates[i].Item1];

                                        if (stateColour.A > 0)
                                        {
                                            graphics.FillPath(fillPath, stateColour);
                                        }

                                        currProb = endProb;
                                    }
                                }
                                else
                                {
                                    GraphicsPath fillPath = new GraphicsPath();
                                    fillPath.Arc(point, size * 0.5, 0, 2 * Math.PI);

                                    if (defaultStateColour.A > 0)
                                    {
                                        graphics.FillPath(fillPath, defaultStateColour);
                                    }
                                }
                            }
                            else
                            {
                                GraphicsPath fillPath = new GraphicsPath();
                                fillPath.Arc(point, size * 0.5, 0, 2 * Math.PI);

                                if (strokeColour.A > 0 && strokeThickness > 0)
                                {
                                    graphics.FillPath(fillPath, defaultStateColour);
                                }
                            }


                            GraphicsPath borderPath = new GraphicsPath();
                            borderPath.Arc(point, size * 0.5, 0, 2 * Math.PI);

                            if (strokeColour.A > 0 && strokeThickness > 0)
                            {
                                graphics.StrokePath(borderPath, strokeColour, strokeThickness, LineCaps.Round, LineJoins.Round);
                            }
                        }
                        //Rectangles
                        else if (type == 1)
                        {
                            List<(object, double)> nodeStates = states[node.Id];

                            if (strokeColour.A > 0 && strokeThickness > 0)
                            {
                                graphics.StrokeRectangle(point.X - width * 0.5 - strokeThickness * 0.48, point.Y - height * 0.5 - strokeThickness * 0.48, width + strokeThickness * 0.96, height + strokeThickness * 0.96, strokeColour, strokeThickness, LineCaps.Round, LineJoins.Round);
                            }

                            if (nodeStates.Count > 0)
                            {
                                double totalProb = 0;

                                for (int i = 0; i < nodeStates.Count; i++)
                                {
                                    totalProb += nodeStates[i].Item2;
                                }

                                if (totalProb != 0)
                                {
                                    double actualWidth = width * Math.Abs(Math.Cos(splitAngle)) + height * Math.Abs(Math.Sin(splitAngle));
                                    double actualHeight = width * Math.Abs(Math.Sin(splitAngle)) + height * Math.Abs(Math.Cos(splitAngle));

                                    graphics.Translate(point.X, point.Y);
                                    graphics.SetClippingPath(-width * 0.5, -height * 0.5, width, height);

                                    graphics.Rotate(splitAngle);
                                    graphics.Scale(actualWidth, actualHeight);

                                    double currProb = 0;

                                    for (int i = 0; i < nodeStates.Count; i++)
                                    {
                                        double endProb = currProb + nodeStates[i].Item2;

                                        Colour stateColour = allStateColours[nodeStates[i].Item1];

                                        if (stateColour.A > 0)
                                        {
                                            if (i < nodeStates.Count - 1 && allStateColours[nodeStates[i + 1].Item1].A == 1)
                                            {
                                                graphics.FillRectangle(-0.5 + currProb / totalProb, -0.5, (endProb - currProb + nodeStates[i + 1].Item2) / totalProb, 1, stateColour);
                                            }
                                            else
                                            {
                                                graphics.FillRectangle(-0.5 + currProb / totalProb, -0.5, (endProb - currProb) / totalProb, 1, stateColour);
                                            }
                                        }

                                        currProb = endProb;
                                    }
                                }
                                else
                                {
                                    graphics.FillRectangle(point.X - width * 0.5, point.Y - height * 0.5, width, height, defaultStateColour);
                                }
                            }
                            else
                            {
                                graphics.FillRectangle(point.X - width * 0.5, point.Y - height * 0.5, width, height, defaultStateColour);
                            }
                        }
                        //Wedges
                        else if (type == 2)
                        {
                            double radius = delta.Modulus();
                            double theta = width / radius;

                            if (!double.IsNaN(theta))
                            {
                                if (double.IsInfinity(theta))
                                {
                                    theta = Math.PI;
                                }

                                while (theta > 2 * Math.PI)
                                {
                                    theta -= 2 * Math.PI;
                                }

                                theta = Math.Min(theta, Math.PI);

                                double innerRadius = Math.Max(0, radius - height * 0.5);
                                double outerRadius = innerRadius + height;

                                Point wedgeCenter = new Point(point.X - radius, point.Y);

                                GraphicsPath wedgePath = new GraphicsPath();
                                wedgePath.MoveTo(wedgeCenter.X + (innerRadius) * Math.Cos(-theta * 0.5), wedgeCenter.Y + (innerRadius) * Math.Sin(-theta * 0.5));
                                wedgePath.Arc(wedgeCenter, innerRadius, -theta * 0.5, theta * 0.5);
                                wedgePath.LineTo(wedgeCenter.X + (outerRadius) * Math.Cos(theta * 0.5), wedgeCenter.Y + (outerRadius) * Math.Sin(theta * 0.5));
                                wedgePath.Arc(wedgeCenter, outerRadius, theta * 0.5, -theta * 0.5);
                                wedgePath.Close();

                                graphics.Save();
                                graphics.SetClippingPath(wedgePath);

                                double wedgeWidth = (outerRadius - (innerRadius) * Math.Cos(theta * 0.5));
                                double wedgeHeight = 2 * (outerRadius) * Math.Sin(theta * 0.5);

                                Point wedgeRectangleCenter = new Point(wedgeCenter.X + (innerRadius) * Math.Cos(theta * 0.5) + wedgeWidth * 0.5, wedgeCenter.Y);

                                List<(object, double)> nodeStates = states[node.Id];


                                if (nodeStates.Count > 0)
                                {
                                    double totalProb = 0;

                                    for (int i = 0; i < nodeStates.Count; i++)
                                    {
                                        totalProb += nodeStates[i].Item2;
                                    }

                                    if (totalProb != 0)
                                    {
                                        double actualWidth = wedgeWidth * Math.Abs(Math.Cos(splitAngle)) + wedgeHeight * Math.Abs(Math.Sin(splitAngle));
                                        double actualHeight = wedgeWidth * Math.Abs(Math.Sin(splitAngle)) + wedgeHeight * Math.Abs(Math.Cos(splitAngle));

                                        graphics.Save();
                                        graphics.Translate(wedgeRectangleCenter);

                                        graphics.Rotate(splitAngle);
                                        graphics.Scale(actualWidth, actualHeight);

                                        double currProb = 0;

                                        for (int i = 0; i < nodeStates.Count; i++)
                                        {
                                            double endProb = currProb + nodeStates[i].Item2;

                                            Colour stateColour = allStateColours[nodeStates[i].Item1];

                                            if (stateColour.A > 0)
                                            {
                                                if (i < nodeStates.Count - 1 && allStateColours[nodeStates[i + 1].Item1].A == 1)
                                                {
                                                    graphics.FillRectangle(-0.5 + currProb / totalProb, -0.5, (endProb - currProb + nodeStates[i + 1].Item2) / totalProb, 1, stateColour);
                                                }
                                                else
                                                {
                                                    graphics.FillRectangle(-0.5 + currProb / totalProb, -0.5, (endProb - currProb) / totalProb, 1, stateColour);
                                                }
                                            }

                                            currProb = endProb;
                                        }
                                        graphics.Restore();
                                    }
                                    else
                                    {
                                        graphics.FillPath(wedgePath, defaultStateColour);
                                    }
                                }
                                else
                                {
                                    graphics.FillPath(wedgePath, defaultStateColour);
                                }

                                graphics.Restore();

                                if (strokeColour.A > 0 && strokeThickness > 0)
                                {
                                    graphics.StrokePath(wedgePath, strokeColour, strokeThickness, LineCaps.Round, LineJoins.Round);
                                }
                            }
                        }

                        graphics.Restore();

                        updateMaxMin(new Point(originalPoint.X - width * 1.5, originalPoint.Y - height * 1.5));
                        updateMaxMin(new Point(originalPoint.X - width * 1.5, originalPoint.Y + height * 1.5));
                        updateMaxMin(new Point(originalPoint.X + width * 1.5, originalPoint.Y + height * 1.5));
                        updateMaxMin(new Point(originalPoint.X + width * 1.5, originalPoint.Y - height * 1.5));
                    }
                }
            }

            if (!everUpdated)
            {
                updateMaxMin(new Point());
            }

            Point topLeft = new Point(minX, minY);
            Point bottomRight = new Point(maxX, maxY);

            return new Point[] { topLeft, bottomRight };
        }

        private static List<(string, double)> GetStatesString(string data, HashSet<string> states)
        {
            List<(string, double)> tbr = new List<(string, double)>();

            if (data[0] != '{')
            {
                string state = data.Substring(0, 1);
                states.Add(state);
                tbr.Add((state, 1));
            }
            else
            {
                string state = data.Substring(1, data.IndexOf("}") - 1);
                string[] splitState = state.Split(',');
                for (int j = 0; j < splitState.Length; j++)
                {
                    string cState = splitState[j].Substring(0, splitState[j].IndexOf(":"));
                    double val = double.Parse(splitState[j].Substring(splitState[j].IndexOf(":") + 1), System.Globalization.CultureInfo.InvariantCulture);
                    states.Add(cState);
                    tbr.Add((cState, val));
                }
            }

            return tbr;
        }

        private static async Task ShowWizardEditWindow(Avalonia.Controls.Window parent, InstanceStateData stateData, string[][] states, int moduleIndex, Colour defaultStateColour, Func<object, Colour?> stateColourFormatter)
        {
            ChildWindow window = new ChildWindow() { Icon = parent.Icon, Title = "Select state colours", Width = 300, WindowStartupLocation = Avalonia.Controls.WindowStartupLocation.CenterOwner, FontFamily = parent.FontFamily, FontSize = 14, SizeToContent = Avalonia.Controls.SizeToContent.Height };

            window.Opened += (s, e) =>
            {
                if (window.Bounds.Height > 600)
                {
                    window.Height = 600;
                }
            };

            Avalonia.Controls.Grid mainContainer = new Avalonia.Controls.Grid() { Margin = new Avalonia.Thickness(0, 0, 0, 10) };
            window.Content = mainContainer;

            mainContainer.RowDefinitions.Add(new Avalonia.Controls.RowDefinition(0, Avalonia.Controls.GridUnitType.Auto));
            mainContainer.RowDefinitions.Add(new Avalonia.Controls.RowDefinition(1, Avalonia.Controls.GridUnitType.Star));
            mainContainer.RowDefinitions.Add(new Avalonia.Controls.RowDefinition(0, Avalonia.Controls.GridUnitType.Auto));


            Avalonia.Controls.Grid titleGrid = new Avalonia.Controls.Grid() { Margin = new Avalonia.Thickness(10) };
            titleGrid.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(0, Avalonia.Controls.GridUnitType.Auto));
            titleGrid.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(1, Avalonia.Controls.GridUnitType.Star));

            Avalonia.Controls.Canvas icon = new Avalonia.Controls.Canvas() { Width = 32, Height = 32 };
            icon.Children.Add(new Avalonia.Controls.Shapes.Ellipse() { Width = 32, Height = 32, Fill = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromRgb(180, 180, 180)) });

            icon.Children.Add(new Avalonia.Controls.Shapes.Path() { Fill = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromRgb(74, 125, 177)), Data = Avalonia.Media.Geometry.Parse("M 16,16 L16,0 A16,16 0 0 1 27.3136,27.3136 Z") });
            icon.Children.Add(new Avalonia.Controls.Shapes.Ellipse() { Width = 32, Height = 32, Stroke = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromRgb(114, 114, 114)), StrokeThickness = 2 });


            titleGrid.Children.Add(icon);

            {
                Avalonia.Controls.TextBlock blk = new Avalonia.Controls.TextBlock() { FontSize = 16, Foreground = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromRgb(0, 114, 178)), Text = "Select state colours", Margin = new Avalonia.Thickness(10, 0, 0, 0), VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center };
                Avalonia.Controls.Grid.SetColumn(blk, 1);
                titleGrid.Children.Add(blk);
            }

            mainContainer.Children.Add(titleGrid);

            Avalonia.Controls.Grid buttonGrid = new Avalonia.Controls.Grid() { Margin = new Avalonia.Thickness(0, 10, 0, 0) };
            buttonGrid.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(1, Avalonia.Controls.GridUnitType.Star));
            buttonGrid.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(0, Avalonia.Controls.GridUnitType.Auto));
            buttonGrid.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(1, Avalonia.Controls.GridUnitType.Star));
            buttonGrid.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(0, Avalonia.Controls.GridUnitType.Auto));
            buttonGrid.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(1, Avalonia.Controls.GridUnitType.Star));
            Avalonia.Controls.Grid.SetRow(buttonGrid, 2);
            mainContainer.Children.Add(buttonGrid);

            Avalonia.Controls.Button okButton = new Avalonia.Controls.Button() { Width = 100, Content = "OK", HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center, FontSize = 13 };
            okButton.Classes.Add("PlainButton");
            Avalonia.Controls.Grid.SetColumn(okButton, 1);
            buttonGrid.Children.Add(okButton);

            Avalonia.Controls.Button cancelButton = new Avalonia.Controls.Button() { Width = 100, Content = "Cancel", HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center, FontSize = 13 };
            cancelButton.Classes.Add("PlainButton");
            Avalonia.Controls.Grid.SetColumn(cancelButton, 3);
            buttonGrid.Children.Add(cancelButton);

            Dictionary<string, Colour> allStateColours = new Dictionary<string, Colour>();

            List<string> allStatesString = (from el in GetAllPossibleStates(states) select string.Join("|", el)).ToList();

            try
            {
                foreach (string sr in allStatesString)
                {
                    allStateColours[sr] = stateColourFormatter(sr) ?? defaultStateColour;
                }
            }
            catch { }

            Avalonia.Controls.ScrollViewer scroller = new Avalonia.Controls.ScrollViewer() { AllowAutoHide = false, VerticalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto, HorizontalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Disabled, Margin = new Avalonia.Thickness(10, 0, 10, 0), Padding = new Avalonia.Thickness(0, 0, 16, 0) };
            Avalonia.Controls.Grid.SetRow(scroller, 1);
            mainContainer.Children.Add(scroller);

            Avalonia.Controls.StackPanel itemsContainer = new Avalonia.Controls.StackPanel();
            scroller.Content = itemsContainer;

            Avalonia.Styling.Style normalStyle = new Style(x => x.OfType<Avalonia.Controls.Grid>().Class("ItemBackground"));
            normalStyle.Setters.Add(new Setter(Avalonia.Controls.Grid.BackgroundProperty, Avalonia.Media.Brushes.Transparent));
            itemsContainer.Styles.Add(normalStyle);

            Avalonia.Styling.Style hoverStyle = new Style(x => x.OfType<Avalonia.Controls.Grid>().Class("ItemBackground").Class(":pointerover"));
            hoverStyle.Setters.Add(new Setter(Avalonia.Controls.Grid.BackgroundProperty, new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromRgb(220, 220, 220))));
            itemsContainer.Styles.Add(hoverStyle);

            List<Avalonia.Controls.Grid> itemList = new List<Avalonia.Controls.Grid>();

            List<Avalonia.Controls.Control> stateLabels = GetStateLabels(states);

            foreach (KeyValuePair<string, Colour> stateColour in allStateColours)
            {
                Avalonia.Controls.Grid grd = new Avalonia.Controls.Grid() { Margin = new Avalonia.Thickness(0, 0, 0, 5) };
                grd.Classes.Add("ItemBackground");
                grd.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(1, Avalonia.Controls.GridUnitType.Star));
                grd.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(100, Avalonia.Controls.GridUnitType.Pixel));

                string key = stateColour.Key;
                Colour col = stateColour.Value;

                int index = allStatesString.IndexOf(key);

                grd.Children.Add(/*new FillingControl<TrimmedTextBox2>(new TrimmedTextBox2()
                {
                    Text = key,
                    HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
                    VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
                    FontSize = 14
                }, 5)*/stateLabels[index]);

                ColorButton colorButton = new ColorButton() { Color = col.ToAvalonia(), VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, RenderTransform = null };
                Avalonia.Controls.Grid.SetColumn(colorButton, 1);
                grd.Children.Add(colorButton);

                colorButton.PropertyChanged += (s, e) =>
                {
                    if (e.Property == ColorButton.ColorProperty)
                    {
                        allStateColours[key] = colorButton.Color.ToVectSharp();
                    }
                };

                colorButton.Classes.Add("PlainButton");

                itemList.Add(grd);
                itemsContainer.Children.Add(grd);
            }

            bool result = false;

            okButton.Click += (s, e) =>
            {
                result = true;
                window.Close();
            };

            cancelButton.Click += (s, e) =>
            {
                window.Close();
            };

            await window.ShowDialog2(parent);

            if (result)
            {
                try
                {
                    string code = @"public static Colour? Format(object attribute)
{
    if (attribute is string state)
    {
        switch (state)
        {
";
                    foreach (KeyValuePair<string, Colour> stateColour in allStateColours)
                    {
                        code += "            case " + System.Text.Json.JsonSerializer.Serialize(stateColour.Key) + ": return Colour.FromRgba(" + stateColour.Value.R.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + stateColour.Value.G.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + stateColour.Value.B.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + stateColour.Value.A.ToString(System.Globalization.CultureInfo.InvariantCulture) + ");\n";
                    }

                    code += @"            default:
                return null;
        }
    }
    else
    {
        return null;
    }
}";

                    object[] formatterParams = new object[2] { code, false };

                    ColourFormatterOptions cfo = new ColourFormatterOptions(code, formatterParams) { AttributeName = "(N/A)", AttributeType = "String", DefaultColour = Colour.FromRgb(220, 220, 220) };

                    await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { stateData.PlottingModulesParameterUpdater(moduleIndex)(new Dictionary<string, object>() { { "State colours:", cfo } }); });

                    typeof(MainWindow).InvokeMember("UpdatePlotLayer", System.Reflection.BindingFlags.InvokeMethod | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, parent, new object[] { moduleIndex, true });
                }
                catch (Exception ex)
                {
                    MessageBox box = new MessageBox("Attention!", "An error occurred while generating the code!\n" + ex.Message);
                    await box.ShowDialog2(parent);
                }
            }
        }

        private static string GetDefaultEnabledCharactersCode(string[][] states)
        {
            System.Text.StringBuilder defaultSourceCode = new System.Text.StringBuilder();

            defaultSourceCode.AppendLine("using PhyloTree;");
            defaultSourceCode.AppendLine("using System.Collections.Generic;");
            defaultSourceCode.AppendLine("using VectSharp;");
            defaultSourceCode.AppendLine("using TreeViewer;");
            defaultSourceCode.AppendLine();
            defaultSourceCode.AppendLine("namespace a" + Guid.NewGuid().ToString().Replace("-", ""));
            defaultSourceCode.AppendLine("{");
            defaultSourceCode.AppendLine("\t//Do not change class name");
            defaultSourceCode.AppendLine("\tpublic static class CustomCode");
            defaultSourceCode.AppendLine("\t{");
            defaultSourceCode.AppendLine("\t\t//Do not change method signature");
            defaultSourceCode.AppendLine("\t\tpublic static bool[] GetEnabledCharacters(string[][] allStates)");
            defaultSourceCode.AppendLine("\t\t{");

            defaultSourceCode.Append("\t\t\treturn new bool[] { ");
            for (int i = 0; i < states.Length; i++)
            {
                defaultSourceCode.Append("true");

                if (i < states.Length - 1)
                {
                    defaultSourceCode.Append(", ");
                }
            }
            defaultSourceCode.AppendLine(" };");
            defaultSourceCode.AppendLine("\t\t}");
            defaultSourceCode.AppendLine("\t}");
            defaultSourceCode.AppendLine("}");

            return defaultSourceCode.ToString();
        }

        private static List<List<string>> GetAllPossibleStates(IReadOnlyList<IEnumerable<string>> characterStates)
        {
            List<List<string>> tbr = new List<List<string>>();

            if (characterStates.Count == 0)
            {

            }
            else if (characterStates.Count == 1)
            {
                foreach (string sr in characterStates[0])
                {
                    tbr.Add(new List<string> { sr });
                }
            }
            else
            {
                List<List<string>> otherStates = GetAllPossibleStates(characterStates.Skip(1).ToArray());

                foreach (string sr in characterStates[0])
                {
                    for (int i = 0; i < otherStates.Count; i++)
                    {
                        List<string> item = new List<string>() { sr };
                        item.AddRange(otherStates[i]);
                        tbr.Add(item);
                    }
                }
            }

            return tbr;
        }

        private static List<List<string>> GetAllPossibleStates(IReadOnlyList<IEnumerable<string>> characterStates, bool[] enabledCharacters)
        {
            List<IEnumerable<string>> enabledCharacterStates = new List<IEnumerable<string>>();

            for (int i = 0; i < characterStates.Count; i++)
            {
                if (enabledCharacters[i])
                {
                    enabledCharacterStates.Add(characterStates[i]);
                }
            }

            return GetAllPossibleStates(enabledCharacterStates);
        }

        private static List<(object, double)> GetMarginalStates(List<List<string>> allPossibleStates, bool[] enabledCharacters, List<(object, double)> states)
        {
            double[] probs = new double[allPossibleStates.Count];

            for (int i = 0; i < states.Count; i++)
            {
                string[] splitState = ((string)states[i].Item1).Split('|');

                List<string> marginalState = new List<string>();

                for (int j = 0; j < enabledCharacters.Length; j++)
                {
                    if (enabledCharacters[j])
                    {
                        marginalState.Add(splitState[j]);
                    }
                }

                for (int j = 0; j < allPossibleStates.Count; j++)
                {
                    if (allPossibleStates[j].SequenceEqual(marginalState))
                    {
                        probs[j] += states[i].Item2;
                        break;
                    }
                }
            }

            List<(object, double)> tbr = new List<(object, double)>();

            for (int i = 0; i < allPossibleStates.Count; i++)
            {
                tbr.Add((string.Join("|", allPossibleStates[i]), probs[i]));
            }

            return tbr;
        }

        private static Colour GetColour(int index, int count)
        {
            TreeViewer.Gradient gradient;

            if (count <= 5)
            {
                gradient = Modules.DefaultGradients["WongDiscrete"];
            }
            else
            {
                gradient = Modules.DefaultGradients["WongRainbow"];
            }

            double pos;

            if (count <= 5)
            {
                pos = Math.Round((double)(index + 1) / count * 5) * 0.2;
            }
            else
            {
                pos = (double)(index + 1) / Math.Max(count, 5);
            }

            return gradient.GetColour(pos);
        }

        private static List<T> GetEnabledStates<T>(IReadOnlyList<T> characterStates, bool[] enabledCharacters) where T : IEnumerable<string>
        {
            List<T> enabledCharacterStates = new List<T>();

            for (int i = 0; i < characterStates.Count; i++)
            {
                if (enabledCharacters[i])
                {
                    enabledCharacterStates.Add(characterStates[i]);
                }
            }

            return enabledCharacterStates;
        }

        private static string GetDefaultStateColours(string[][] states)
        {
            List<string> allStates = (from el in GetAllPossibleStates(states) select string.Join("|", el)).ToList();

            System.Text.StringBuilder defaultSourceCode = new System.Text.StringBuilder();
            defaultSourceCode.AppendLine("public static Colour? Format(object attribute)");
            defaultSourceCode.AppendLine("{");
            defaultSourceCode.AppendLine("\tif (attribute is string state)");
            defaultSourceCode.AppendLine("\t{");
            defaultSourceCode.AppendLine("\t\tswitch (state)");
            defaultSourceCode.AppendLine("\t\t{");


            for (int i = 0; i < allStates.Count; i++)
            {
                defaultSourceCode.Append("\t\t\tcase \"");
                defaultSourceCode.Append(allStates[i]);
                defaultSourceCode.Append("\": return Colour.FromRgb(");

                Colour col = GetColour(i, allStates.Count);

                defaultSourceCode.Append(Math.Round(col.R * 255));
                defaultSourceCode.Append(", ");
                defaultSourceCode.Append(Math.Round(col.G * 255));
                defaultSourceCode.Append(", ");
                defaultSourceCode.Append(Math.Round(col.B * 255));

                defaultSourceCode.AppendLine(");");
            }


            defaultSourceCode.AppendLine("\t\t\tdefault:");
            defaultSourceCode.AppendLine("\t\t\t\treturn null;");
            defaultSourceCode.AppendLine("\t\t}");
            defaultSourceCode.AppendLine("\t}");
            defaultSourceCode.AppendLine("\telse");
            defaultSourceCode.AppendLine("\t{");
            defaultSourceCode.AppendLine("\t\treturn null;");
            defaultSourceCode.AppendLine("\t}");
            defaultSourceCode.AppendLine("}");

            return defaultSourceCode.ToString();
        }

        private static async Task ShowWizardEditEnabledCharactersWindow(Avalonia.Controls.Window parent, InstanceStateData stateData, string[][] states, bool[] enabledCharacters, int moduleIndex)
        {
            ChildWindow window = new ChildWindow() { Icon = parent.Icon, Title = "Select enabled characters", Width = 320, WindowStartupLocation = Avalonia.Controls.WindowStartupLocation.CenterOwner, FontFamily = parent.FontFamily, FontSize = 14, SizeToContent = Avalonia.Controls.SizeToContent.Height };

            window.Opened += (s, e) =>
            {
                if (window.Bounds.Height > 600)
                {
                    window.Height = 600;
                }
            };

            Avalonia.Controls.Grid mainContainer = new Avalonia.Controls.Grid() { Margin = new Avalonia.Thickness(0, 0, 0, 10) };
            window.Content = mainContainer;

            mainContainer.RowDefinitions.Add(new Avalonia.Controls.RowDefinition(0, Avalonia.Controls.GridUnitType.Auto));
            mainContainer.RowDefinitions.Add(new Avalonia.Controls.RowDefinition(1, Avalonia.Controls.GridUnitType.Star));
            mainContainer.RowDefinitions.Add(new Avalonia.Controls.RowDefinition(0, Avalonia.Controls.GridUnitType.Auto));


            Avalonia.Controls.Grid titleGrid = new Avalonia.Controls.Grid() { Margin = new Avalonia.Thickness(10) };
            titleGrid.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(0, Avalonia.Controls.GridUnitType.Auto));
            titleGrid.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(1, Avalonia.Controls.GridUnitType.Star));

            Avalonia.Controls.Canvas icon = new Avalonia.Controls.Canvas() { Width = 32, Height = 32 };
            icon.Children.Add(new Avalonia.Controls.Shapes.Ellipse() { Width = 32, Height = 32, Fill = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromRgb(180, 180, 180)) });

            icon.Children.Add(new Avalonia.Controls.Shapes.Path() { Fill = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromRgb(74, 125, 177)), Data = Avalonia.Media.Geometry.Parse("M 16,16 L16,0 A16,16 0 0 1 27.3136,27.3136 Z") });
            icon.Children.Add(new Avalonia.Controls.Shapes.Ellipse() { Width = 32, Height = 32, Stroke = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromRgb(114, 114, 114)), StrokeThickness = 2 });


            titleGrid.Children.Add(icon);

            {
                Avalonia.Controls.TextBlock blk = new Avalonia.Controls.TextBlock() { FontSize = 16, Foreground = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromRgb(0, 114, 178)), Text = "Select enabled characters", Margin = new Avalonia.Thickness(10, 0, 0, 0), VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center };
                Avalonia.Controls.Grid.SetColumn(blk, 1);
                titleGrid.Children.Add(blk);
            }

            mainContainer.Children.Add(titleGrid);

            Avalonia.Controls.Grid buttonGrid = new Avalonia.Controls.Grid() { Margin = new Avalonia.Thickness(0, 10, 0, 0) };
            buttonGrid.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(1, Avalonia.Controls.GridUnitType.Star));
            buttonGrid.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(0, Avalonia.Controls.GridUnitType.Auto));
            buttonGrid.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(1, Avalonia.Controls.GridUnitType.Star));
            buttonGrid.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(0, Avalonia.Controls.GridUnitType.Auto));
            buttonGrid.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(1, Avalonia.Controls.GridUnitType.Star));
            Avalonia.Controls.Grid.SetRow(buttonGrid, 2);
            mainContainer.Children.Add(buttonGrid);

            Avalonia.Controls.Button okButton = new Avalonia.Controls.Button() { Width = 100, Content = "OK", HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center, FontSize = 13 };
            okButton.Classes.Add("PlainButton");
            Avalonia.Controls.Grid.SetColumn(okButton, 1);
            buttonGrid.Children.Add(okButton);

            Avalonia.Controls.Button cancelButton = new Avalonia.Controls.Button() { Width = 100, Content = "Cancel", HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center, FontSize = 13 };
            cancelButton.Classes.Add("PlainButton");
            Avalonia.Controls.Grid.SetColumn(cancelButton, 3);
            buttonGrid.Children.Add(cancelButton);

            List<string> allStatesString = (from el in GetAllPossibleStates(states) select string.Join("|", el)).ToList();

            Avalonia.Controls.ScrollViewer scroller = new Avalonia.Controls.ScrollViewer() { AllowAutoHide = false, VerticalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto, HorizontalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Disabled, Margin = new Avalonia.Thickness(10, 0, 10, 0), Padding = new Avalonia.Thickness(0, 0, 16, 0) };
            Avalonia.Controls.Grid.SetRow(scroller, 1);
            mainContainer.Children.Add(scroller);

            Avalonia.Controls.StackPanel itemsContainer = new Avalonia.Controls.StackPanel();
            scroller.Content = itemsContainer;

            Avalonia.Styling.Style normalStyle = new Style(x => x.OfType<Avalonia.Controls.Grid>().Class("ItemBackground"));
            normalStyle.Setters.Add(new Setter(Avalonia.Controls.Grid.BackgroundProperty, Avalonia.Media.Brushes.Transparent));
            itemsContainer.Styles.Add(normalStyle);

            Avalonia.Styling.Style hoverStyle = new Style(x => x.OfType<Avalonia.Controls.Grid>().Class("ItemBackground").Class(":pointerover"));
            hoverStyle.Setters.Add(new Setter(Avalonia.Controls.Grid.BackgroundProperty, new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromRgb(220, 220, 220))));
            itemsContainer.Styles.Add(hoverStyle);

            List<Avalonia.Controls.Grid> itemList = new List<Avalonia.Controls.Grid>();

            List<Avalonia.Controls.Control> characterLabels = GetCharacterLabels(states);

            for (int i = 0; i < states.Length; i++)
            {
                Avalonia.Controls.Grid grd = new Avalonia.Controls.Grid() { Margin = new Avalonia.Thickness(0, 0, 0, 5) };
                grd.Classes.Add("ItemBackground");
                grd.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(0, Avalonia.Controls.GridUnitType.Auto));
                grd.ColumnDefinitions.Add(new Avalonia.Controls.ColumnDefinition(1, Avalonia.Controls.GridUnitType.Star));

                int index = i;

                Avalonia.Controls.Grid.SetColumn(characterLabels[index], 1);
                grd.Children.Add(characterLabels[index]);

                Avalonia.Controls.CheckBox box = new Avalonia.Controls.CheckBox() { Content = i.ToString(), FontSize = 14, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, VerticalContentAlignment = Avalonia.Layout.VerticalAlignment.Center, IsChecked = enabledCharacters[i], Margin = new Avalonia.Thickness(10, 0, 20, 0) };

                grd.Children.Add(box);

                box.PropertyChanged += (s, e) =>
                {
                    if (e.Property == Avalonia.Controls.CheckBox.IsCheckedProperty)
                    {
                        enabledCharacters[index] = box.IsChecked == true;
                    }
                };

                itemList.Add(grd);
                itemsContainer.Children.Add(grd);
            }

            bool result = false;

            okButton.Click += (s, e) =>
            {
                result = true;
                window.Close();
            };

            cancelButton.Click += (s, e) =>
            {
                window.Close();
            };

            await window.ShowDialog2(parent);

            if (result)
            {
                try
                {
                    string[][] enabledCharacterStates = GetEnabledStates(states, enabledCharacters).ToArray();

                    string code = GetDefaultStateColours(enabledCharacterStates);

                    object[] formatterParams = new object[2] { code, false };

                    ColourFormatterOptions cfo = new ColourFormatterOptions(code, formatterParams) { AttributeName = "(N/A)", AttributeType = "String", DefaultColour = Colour.FromRgb(220, 220, 220) };

                    await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => { stateData.PlottingModulesParameterUpdater(moduleIndex)(new Dictionary<string, object>() { { "Enabled characters:", new CompiledCode(GetEnabledCharactersCode(enabledCharacters)) }, { "State colours:", cfo } }); });

                    typeof(MainWindow).InvokeMember("UpdatePlotLayer", System.Reflection.BindingFlags.InvokeMethod | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, parent, new object[] { moduleIndex, true });
                }
                catch (Exception ex)
                {
                    MessageBox box = new MessageBox("Attention!", "An error occurred while generating the code!\n" + ex.Message);
                    await box.ShowDialog2(parent);
                }
            }
        }

        private static string GetEnabledCharactersCode(bool[] enabledCharacters)
        {
            System.Text.StringBuilder defaultSourceCode = new System.Text.StringBuilder();

            defaultSourceCode.AppendLine("using PhyloTree;");
            defaultSourceCode.AppendLine("using System.Collections.Generic;");
            defaultSourceCode.AppendLine("using VectSharp;");
            defaultSourceCode.AppendLine("using TreeViewer;");
            defaultSourceCode.AppendLine();
            defaultSourceCode.AppendLine("namespace a" + Guid.NewGuid().ToString().Replace("-", ""));
            defaultSourceCode.AppendLine("{");
            defaultSourceCode.AppendLine("\t//Do not change class name");
            defaultSourceCode.AppendLine("\tpublic static class CustomCode");
            defaultSourceCode.AppendLine("\t{");
            defaultSourceCode.AppendLine("\t\t//Do not change method signature");
            defaultSourceCode.AppendLine("\t\tpublic static bool[] GetEnabledCharacters(string[][] allStates)");
            defaultSourceCode.AppendLine("\t\t{");

            defaultSourceCode.Append("\t\t\treturn new bool[] { ");
            for (int i = 0; i < enabledCharacters.Length; i++)
            {
                if (enabledCharacters[i])
                {
                    defaultSourceCode.Append("true");
                }
                else
                {
                    defaultSourceCode.Append("false");
                }

                if (i < enabledCharacters.Length - 1)
                {
                    defaultSourceCode.Append(", ");
                }
            }
            defaultSourceCode.AppendLine(" };");
            defaultSourceCode.AppendLine("\t\t}");
            defaultSourceCode.AppendLine("\t}");
            defaultSourceCode.AppendLine("}");

            return defaultSourceCode.ToString();
        }



        private static Colour BlendWithWhite(Colour col, double intensity)
        {
            return Colour.FromRgb(col.R * intensity + 1 - intensity, col.G * intensity + 1 - intensity, col.B * intensity + 1 - intensity);
        }

        private static List<Avalonia.Controls.Control> GetStateLabels(string[][] states)
        {
            List<List<string>> allPossibleStates = GetAllPossibleStates(states);

            List<Avalonia.Controls.Control> tbr = new List<Avalonia.Controls.Control>();

            for (int i = 0; i < allPossibleStates.Count; i++)
            {
                Avalonia.Controls.StackPanel pnl = new Avalonia.Controls.StackPanel() { Orientation = Avalonia.Layout.Orientation.Horizontal, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center };

                for (int j = 0; j < allPossibleStates[i].Count; j++)
                {
                    Avalonia.Controls.Grid grd = new Avalonia.Controls.Grid() { Background = new Avalonia.Media.SolidColorBrush(BlendWithWhite(GetColour(Array.IndexOf(states[j], allPossibleStates[i][j]), states[j].Length), 0.25).ToAvalonia()) };

                    if (j > 0)
                    {
                        grd.Margin = new Avalonia.Thickness(2, 0, 0, 0);
                    }

                    grd.Children.Add(new Avalonia.Controls.TextBlock() { FontSize = 14, Text = allPossibleStates[i][j], Margin = new Avalonia.Thickness(5, 0), FontFamily = new Avalonia.Media.FontFamily("resm:TreeViewer.Fonts.?assembly=TreeViewer#Roboto Mono") });

                    pnl.Children.Add(grd);
                }

                tbr.Add(pnl);
            }

            return tbr;

        }

        private static List<Avalonia.Controls.Control> GetCharacterLabels(string[][] states)
        {
            List<Avalonia.Controls.Control> tbr = new List<Avalonia.Controls.Control>();

            for (int i = 0; i < states.Length; i++)
            {
                Avalonia.Controls.StackPanel pnl = new Avalonia.Controls.StackPanel() { Orientation = Avalonia.Layout.Orientation.Horizontal, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center };

                for (int j = 0; j < states[i].Length; j++)
                {
                    Avalonia.Controls.Grid grd = new Avalonia.Controls.Grid() { Background = new Avalonia.Media.SolidColorBrush(BlendWithWhite(GetColour(Array.IndexOf(states[i], states[i][j]), states[i].Length), 0.25).ToAvalonia()) };

                    if (j > 0)
                    {
                        grd.Margin = new Avalonia.Thickness(2, 0, 0, 0);
                    }

                    grd.Children.Add(new Avalonia.Controls.TextBlock() { FontSize = 14, Text = states[i][j], Margin = new Avalonia.Thickness(5, 0), FontFamily = new Avalonia.Media.FontFamily("resm:TreeViewer.Fonts.?assembly=TreeViewer#Roboto Mono") });

                    pnl.Children.Add(grd);
                }

                tbr.Add(pnl);
            }

            return tbr;

        }
    }
}

